#include <QtGui>
#include <iostream>
#include <math.h>
#include "FractalViewer.h"

// default scale
const int cDefScale = 100;
// default window size
const QSize cDefSize(500, 400);
// default max iterations
const int cDefMaxIter = 250;
// default antialiasing flag
const bool cDefAntialiasing = false;
// zoom in factor
const double cZoomFac = 1.1;
// scroll step as a ration of current window size
const double cScrollStep = 0.025;
// inc/dec step for max iterations
const int cMaxiterStep = 10;

QList<ColorPalette*> FractalViewer::palettes;
QHash<QString, TColorCalc> FractalViewer::colorCalcs;

FractalViewer::FractalViewer(QWidget *parent) 
: QWidget(parent)
{
    // intialize all the static properties such as palettes and colorcalcs
	initialize();
	
	// reset all properties
	curPalette = FractalViewer::palettes.begin();
	curColorCalc = FractalViewer::colorCalcs.begin();
	centerx = 0;
	centery = 0;
    mScale = pixmapScale = cDefScale;
	mMaxiter = cDefMaxIter;
	mAntialiasing = cDefAntialiasing;
	
	// for marshalling in QueuedConnection that is being used for inter-thread communication
    qRegisterMetaType<QImage>("QImage");
    qRegisterMetaType<TRenderParams>("TRenderParams");
    
	// connect to reasulting image from thread
	connect(&thread, 
			SIGNAL(renderedImage(const QImage &, const TRenderParams &)),
            this, 
			SLOT(updatePixmap(const QImage &, const TRenderParams &)));
	
	// initialize window
	setAttribute(Qt::WA_QuitOnClose, true);
    setWindowTitle("Fractacular");
    setCursor(Qt::CrossCursor);
    resize(cDefSize);	
}

void FractalViewer::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
	painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
    painter.fillRect(rect(), Qt::black);
	
    if (pixmap.isNull()) {
		// initial startup
        painter.setPen(Qt::white);
        painter.drawText(rect(), Qt::AlignCenter, "Rendering initial image, please wait...");
        return;
    }

    if (scale() == pixmapScale) {
        painter.drawPixmap(pixmapOffset, pixmap);
    } else {
        double scaleFactor = scale() / pixmapScale;
        int newWidth = int(pixmap.width() * scaleFactor);
        int newHeight = int(pixmap.height() * scaleFactor);
        int newX = pixmapOffset.x() + (pixmap.width() - newWidth) / 2;
        int newY = pixmapOffset.y() + (pixmap.height() - newHeight) / 2;
		
        painter.save();
        painter.translate(newX, newY);
        painter.scale(scaleFactor, scaleFactor);
        QRectF exposed = painter.matrix().inverted().mapRect(rect()).adjusted(-1, -1, 1, 1);
        painter.drawPixmap(exposed, pixmap, exposed);
        painter.restore();
    }	

	if (!lastMessage.isEmpty()) {
		// draws the box around the message
		QFontMetrics metrics = painter.fontMetrics();
		int tw = metrics.width(lastMessage);
		int th = metrics.lineSpacing();
		int hp = 50; // horizontal padding
		int vp = 40; // vertical padding
 		int bm = (int) height()*0.2; // bottom margin
		int tx = (width() - tw) / 2; // text X
		int ty = height() - bm - th - metrics.leading() - metrics.ascent(); // text Y
		painter.setPen(Qt::NoPen);
		painter.setBrush(QColor(0, 0, 0, 127));
		// FIXME: from whatever reason the height of the box i wrong???
		painter.drawRoundedRect(tx - hp, ty - vp, tw + 2*hp, th + 2*vp, 10.0, 10.0);
		painter.setPen(Qt::white);
		painter.drawText(tx, ty, lastMessage);
		// FIXME: this is probably not the best way to go since when it is queued it can disapper much quicker that it should, 
		// but to implement it correctly one will need to create a special QTimer that can emit also what text it should hide 
		QTimer::singleShot(1500, this, SLOT(hideLastMessage()));		
	}

// some simple debugging messages
#ifdef DEBUG
    painter.setPen(Qt::NoPen);
	painter.drawLine(size().rwidth()/2,0,size().rwidth()/2,size().rheight());
	painter.drawLine(0,size().rheight()/2,size().rwidth(),size().rheight()/2);
	
    QString text = QString("s=%1 ps=%2 x=%3 y=%4 max=%5 aa=%6").arg(scale()).arg(pixmapScale).arg(centerx).arg(centery).arg(max_itererations()).arg(antialiasing());
    QFontMetrics metrics = painter.fontMetrics();
    int textWidth = metrics.width(text);
	
    painter.setBrush(QColor(0, 0, 0, 127));
    painter.drawRect((width() - textWidth) / 2 - 5, 0, textWidth + 10, metrics.lineSpacing() + 5);
    painter.setPen(Qt::white);
    painter.drawText((width() - textWidth) / 2, metrics.leading() + metrics.ascent(), text);
#endif
}

void FractalViewer::resizeEvent(QResizeEvent * /* event */)
{
   rerender();
}

void FractalViewer::keyPressEvent(QKeyEvent *event)
{
    switch (event->key()) {
		case Qt::Key_Q:
			close();
			break;
		case Qt::Key_F:
			if (isFullScreen()) {
				showNormal();
			} else {
				showFullScreen();
			}
			break;
		case Qt::Key_R:
			pixmapOffset = QPoint();
			centerx = 0;
			centery = 0;
			setScale(cDefScale);
			break;
		case Qt::Key_S:
			saveImage();
			break;
		case Qt::Key_Greater:
			setMaxiter(maxiter()+cMaxiterStep);
			break;
		case Qt::Key_Less:
			if (maxiter()-cMaxiterStep > 0) {
				setMaxiter(maxiter()-cMaxiterStep);
			}
			break;
		case Qt::Key_Period:
			if ((curPalette + 1) == FractalViewer::palettes.end()) {
				curPalette = FractalViewer::palettes.begin();
			} else {
				++curPalette;
			}
			
			setLastMessage(QString("Palette: %1").arg((*curPalette)->name()));
			rerender();	
			break;
		case Qt::Key_Comma:
			if (curPalette == FractalViewer::palettes.begin()) {
				curPalette = FractalViewer::palettes.end();
			}
			--curPalette;
			
			setLastMessage(QString("Palette: %1").arg((*curPalette)->name()));
			rerender();	
			break;
		case Qt::Key_BracketRight:
			if ((curColorCalc + 1) == FractalViewer::colorCalcs.end()) {
				curColorCalc = FractalViewer::colorCalcs.begin();
			} else {
				++curColorCalc;
			}
			
			setLastMessage(QString("Color assess: %1").arg(curColorCalc.key()));
			rerender();	
			break;
		case Qt::Key_BracketLeft:
			if (curColorCalc == FractalViewer::colorCalcs.begin()) {
				curColorCalc = FractalViewer::colorCalcs.end();
			}
			--curColorCalc;
			
			setLastMessage(QString("Color assess: %1").arg(curColorCalc.key()));
			rerender();	
			break;
		case Qt::Key_A:
			if (event->modifiers() & Qt::ShiftModifier) {
				setAntialiasing(!antialiasing());
			} else {
				zoom(cZoomFac);
			}
			break;
		case Qt::Key_Z:
			zoom(1/cZoomFac);	
			break;
		case Qt::Key_Left:
			scroll((int)size().rwidth()*cScrollStep, 0);
			break;
		case Qt::Key_Right:
			scroll((int)-size().rwidth()*cScrollStep, 0);
			break;
		case Qt::Key_Down:
			scroll(0, (int)-size().rheight()*cScrollStep);
			break;
		case Qt::Key_Up:
			scroll(0, (int)size().rheight()*cScrollStep);
			break;
		default:
			QWidget::keyPressEvent(event);
    }
}

void FractalViewer::saveImage() 
{
	QString fileName = QFileDialog::getSaveFileName(this, "Save image as...", NULL, "PNG Files (*.png)");
	if (!fileName.isEmpty()) {
		pixmap.save(fileName, "PNG");
	}				
}

void FractalViewer::wheelEvent(QWheelEvent *event)
{
	if (event->orientation() == Qt::Vertical) {
		// this is the default conversion suggested by QT
		// from docs: Most mouse types work in steps of 15 degrees, 
		// in which case the delta value is a multiple of 120; i.e., 120 * 1/8 = 15
		int numDegrees = event->delta() / 8;
		double numSteps = numDegrees / 15.0;
		
		// if it's negative it's a scroll backward = zoom out
		double newZoom = (numSteps < 0 ? 1/cZoomFac : cZoomFac);
		QPoint center(size().rwidth() / 2, size().rheight() / 2);
		QPoint d = (center-event->pos())-(center-event->pos())*newZoom;
		scroll(d);
		zoom(newZoom);
		event->accept();
	}
}

void FractalViewer::mouseDoubleClickEvent(QMouseEvent *event) {
	if (event->button() == Qt::LeftButton) {
		QPoint center(size().rwidth() / 2, size().rheight() / 2);
		QPoint newPos = (event->pos()-center);

        lastDragPos *= 0;
		scroll(newPos);
	}
}

void FractalViewer::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        lastDragPos = event->pos();
	}
}

void FractalViewer::mouseMoveEvent(QMouseEvent *event)
{
    if (event->buttons() & Qt::LeftButton) {
        QPoint d = lastDragPos - event->pos();
        lastDragPos = event->pos();
        
		scroll(d);
    }
}

void FractalViewer::mouseReleaseEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton && !lastDragPos.isNull()) {
		QPoint d = event->pos() - lastDragPos;
        lastDragPos *= 0;
		
        scroll(d);
    }
}

void FractalViewer::updatePixmap(const QImage &image, const TRenderParams &renderParams)
{	
    pixmap = QPixmap::fromImage(image);
    pixmapScale = renderParams.scale;
    pixmapOffset *= 0;

    update();

	delete renderParams.renderer;
}

void FractalViewer::zoom(double zoom)
{
	if (zoom > 0) {
		double newScale = scale() * zoom;
		
		centerx *= zoom;
		centery *= zoom;
		pixmapOffset *= zoom;

//		std::cout << oldScale << "," << newScale << "," << " " << centerx << "," << centery << " " << pixmapOffset.x() << "," << pixmapOffset.y() << std::endl;
		
		setScale(newScale);
	}
}

void FractalViewer::scroll(const QPoint &pos)
{
	scroll(pos.x(), pos.y());
}

void FractalViewer::scroll(int dx, int dy)
{
	centerx += dx;
	centery += dy;
    pixmapOffset -= QPoint(dx,dy);	
	
	rerender();
}

void FractalViewer::rerender() 
{
    update();
	
	TRenderParams renderParams;
	renderParams.centerx = centerx;
	renderParams.centery = centery;
	renderParams.size = size();
	renderParams.scale = scale();
	renderParams.antialiasing = antialiasing();
	renderParams.maxiter = maxiter();	
	renderParams.renderer = new DefaultRenderer(*curPalette, curColorCalc.value());
	
    thread.render(renderParams);
}

// -- PROPERTIES

double FractalViewer::scale() const {
	return mScale;
}

int FractalViewer::maxiter() const {
	return mMaxiter;
}

bool FractalViewer::antialiasing() const {
	return mAntialiasing;
}

void FractalViewer::setAntialiasing(bool antialiasing)
{
    mAntialiasing = antialiasing;
	setLastMessage(QString("Antialiasing: %1").arg(mAntialiasing ? "on" : "off"));
	
	rerender();
}

void FractalViewer::setScale(double scale)
{
    mScale = scale;
	setLastMessage(QString("Scale: %1").arg(mScale));
	
	rerender();
}

void FractalViewer::setMaxiter(int maxiter)
{
    mMaxiter = maxiter;
	setLastMessage(QString("Maximum interation: %1").arg(mMaxiter));

	rerender();
}

void FractalViewer::setLastMessage(const QString& message)
{
	lastMessage = message;
	update();
}

void FractalViewer::hideLastMessage()
{
	if (!lastMessage.isEmpty()) {
		lastMessage.clear();
		update();
	}
}

// --STATICS

void FractalViewer::initialize()
{
	if (FractalViewer::palettes.isEmpty()) {
		FractalViewer::palettes << new DefaultColorPalette;
		FractalViewer::palettes << new MappingColorPalette("Blues", &cPaletteBlues); 
		FractalViewer::palettes << new MappingColorPalette("Gold", &cPaletteGold);
		FractalViewer::palettes << new MappingColorPalette("Greens", &cPaletteGreens); 
		FractalViewer::palettes << new MappingColorPalette("Ice", &cPaletteIce);
		FractalViewer::palettes << new MappingColorPalette("Juteblue", &cPaletteJutebleu); 
		FractalViewer::palettes << new MappingColorPalette("Jutemap", &cPaletteJutemap);
		FractalViewer::palettes << new MappingColorPalette("Jutes", &cPaletteJutes);
		FractalViewer::palettes << new MappingColorPalette("Mandmap", &cPaletteMandmap); 
		FractalViewer::palettes << new MappingColorPalette("Phong", &cPalettePhong); 
		FractalViewer::palettes << new MappingColorPalette("Rose", &cPaletteRose);
	}
	
	if (FractalViewer::colorCalcs.isEmpty()) {
		FractalViewer::colorCalcs["Iterations only"] = &fromIteration;
		FractalViewer::colorCalcs["Normalized Iteration Count Algorithm"] = &fromNICA;
		FractalViewer::colorCalcs["Real part"] = &fromRealPart;
		FractalViewer::colorCalcs["Imaginary part"] = &fromImaginaryPart;
		FractalViewer::colorCalcs["Sum of real and imaginary part"] = &fromSum;
		FractalViewer::colorCalcs["Angle"] = &fromAngle;
	}
}

ColorPalette::ColorPalette(const char *name)
{
	this->mName = name;
}

inline QString ColorPalette::name() const
{
	return mName;
}

DefaultColorPalette::DefaultColorPalette()
: ColorPalette("Default")
{
}

inline QRgb DefaultColorPalette::getColor(double d)
{
	// default color derivation
	int r = (int) 127 + 127.0 * sin(d / 30.0);
	int g = (int) d * 20.0;
	int b = (int) 127 - 127.0 * sin(d / 30.0);
	
	return qRgb(r,g,b);
}

MappingColorPalette::MappingColorPalette(const char *name, const TColorMap *map)
: ColorPalette(name)
{
	this->map = map;
}

QRgb MappingColorPalette::getColor(double d)
{
	int i = ((int)d) % cPaletteSize;
	int r = (*map)[i][0];
	int g = (*map)[i][1];
	int b = (*map)[i][2];
	
	return qRgb(r,g,b);
}

DefaultRenderer::DefaultRenderer(ColorPalette *palette, TColorCalc colorCalc)
{
	this->palette = palette;
	this->colorCalc = colorCalc;
}

QRgb DefaultRenderer::render(int iter, double zx, double zy)
{
	return palette->getColor(colorCalc(iter, zx, zy));
}